import axios from "axios";
import dayjs from "dayjs";
import fs from "node:fs";
import crypto from 'crypto';
import probe from 'probe-image-size';
import { getCacheRecord, updateCacheRecord } from "../../helper";

export class Uploader {
  static CHUNK_SIZE = 8 * 1024 * 1024;
  static SINGLE_FILE_SIZE = 10485760;
  static CONCURRENT_LIMIT = 4;
  static REPLACE_HOST = 'https://finder.video.qq.com';
  static CACHE_FILE_PATH = 'weixin_file.json';
  static CACHE_EXPIRATION_TIME = 60 * 60 * 1000;

  host = 'https://finderpre.video.qq.com'
  useBackUpHost = false

  constructor(uploadParams, envInfo = []) {
    this.uploadParams = uploadParams
    this.envInfo = envInfo
  }

  async upload(file, cache = true) {
    if (cache) {
      const cachedFile = this.getResponseByCache(file.path, Uploader.CACHE_FILE_PATH);
  
      if (cachedFile) {
        return cachedFile;
      }
    }

    const fileSize = await file.getFileSize()
    const uploadID = await this.getUploadId(file)
    const isBigFile = fileSize > Uploader.SINGLE_FILE_SIZE;

    let uploadedChunks = [];
    let transFlag = '';
    let traceInfo = {
      uploadCdnStart: dayjs().unix(),
    }

    if (isBigFile) {
      [ transFlag, uploadedChunks ] = await this.concurrentLoadChunk(file, uploadID);
    } else {
      const uploadedChunk = await this.loadChunk(file, uploadID);

      if (uploadedChunk) {
        uploadedChunks.push(uploadedChunk);
        transFlag = uploadedChunk['TransFlag'];
      }
    }

    uploadedChunks = uploadedChunks.filter(Boolean);

    console.log('Platform:Weixinshipin:uploadChunks', {
      uploadedChunks,
      transFlag,
    });

    const chunkCount = Math.ceil(fileSize / Uploader.CHUNK_SIZE);

    for (let i = 0; i < chunkCount; i++) {
      const uploadedChunk = uploadedChunks.find(chunk => chunk.PartNumber === i + 1);

      if (!uploadedChunk) {
        const response = await this.loadChunk(file, uploadID, i, true);

        if (response) {
          uploadedChunks.push(response);
          transFlag = response.TransFlag;
        }
      }
    }

    // 验证所有分片是否上传完成
    if (uploadedChunks.length !== chunkCount) {
      throw new Error('微信视频号文件上传失败');
    }

    uploadedChunks = uploadedChunks.sort((a, b) => a.PartNumber - b.PartNumber)

    try {
      const response = await this.finishUpload(file, uploadID, uploadedChunks, transFlag);

      // 替换 URL 中的域名
      response.httpsUrl = response.DownloadURL.replace('http://wxapp.tc.qq.com', Uploader.REPLACE_HOST);

      // 如果是视频文件，记录上传结束时间
      if (file.cdnFileType === 'videoFileType') {
        traceInfo.uploadCdnEnd = Math.floor(Date.now() / 1000);
      }

      // 添加文件信息
      response.size = await file.getFileSize();
      response.traceInfo = traceInfo;
      response.md5 = await file.getMd5();

      // 如果是图片文件，获取图片尺寸
      if (file.cdnFileType === 'pictureFileType') {
        const input = fs.createReadStream(file.path);
        
        try {
          const dimensions = await probe(input);
          response.width = dimensions.width;
          response.height = dimensions.width;
        } finally {
          input.destroy();
        }
      }

      if (cache) {
        updateCacheRecord(file.path, JSON.stringify(response), Uploader.CACHE_FILE_PATH);
      }

      return response;
    } catch (error) {
      console.error('Platform:Weixinshipin:FinishUpload:Exception', {
        file: error.fileName,
        line: error.lineNumber,
        message: error.message,
        stack: error.stack
      });

      throw new Error('微信视频号文件上传失败');
    }
  }

  async finishUpload(file, uploadID, uploadedChunks, transFlag){
    // 只保留需要的字段
    const partInfo = uploadedChunks.map(chunk => ({
      PartNumber: chunk.PartNumber,
      ETag: chunk.ETag
    }));

    const { data: response } = await axios({
      method: 'POST',
      url: `${this.host}/completepartuploaddfs`,
      params: {
        UploadID: uploadID,
      },
      data: {
        PartInfo: partInfo,
        TransFlag: transFlag,
      },
      headers: await this.getUploadHeader(file)
    });

    console.log('Platform:Weixinshipin:FinishUpload:Response', response);

    return response;
  }

  async loadChunk(file, uploadID, index = 0, quickUpload = false) {
    const fileSize = await file.getFileSize()
    const chunkCount = Math.ceil(fileSize / Uploader.CHUNK_SIZE);
    const indexSize = index * Uploader.CHUNK_SIZE;
    const chunkSize = Math.min(Uploader.CHUNK_SIZE, fileSize - indexSize);

    const fileHandle = await fs.promises.open(file.path, 'r');
    const buffer = Buffer.alloc(chunkSize);
    
    await fileHandle.read(buffer, 0, chunkSize, indexSize);
    await fileHandle.close();

    const chunk = {
      content: buffer,
      size: chunkSize,
      index: index,
      file: file,
    }

    const pRetry = (...args) => import('p-retry').then(({default: retry}) => retry(...args));

    const response = await pRetry(() => {
      return this.uploadChunk(chunk, uploadID, (! quickUpload) && (! (index === 0 || index > (chunkCount - Uploader.CONCURRENT_LIMIT))))
    }, {
      retries: 3
    });

    if (! response.ETag){
      return false;
    }

    return {
      ...response,
     PartNumber: index + 1,
     Size: chunkSize,
    }
  }

  async concurrentLoadChunk(file, uploadID) {
    const fileSize = await file.getFileSize()
    const chunkCount = Math.ceil(fileSize / Uploader.CHUNK_SIZE);

    let transFlag = '';

    try {
      const tasks = Array.from({ length: chunkCount }, async (_, i) => {
        const response = await this.loadChunk(file, uploadID, i);

        if (response && response.TransFlag) {
          transFlag = response.TransFlag;
        }

        return response;
      });

      // 限制并发数
      const results = [];
      for (let i = 0; i < chunkCount; i += Uploader.CONCURRENT_LIMIT) {
        const batch = tasks.slice(i, i + Uploader.CONCURRENT_LIMIT);
        const batchResults = await Promise.all(batch);
        results.push(...batchResults);
      }

      return [transFlag, results];
    } catch (error) {
      console.error('Platform:Weixinshipin:UploadChunk:Exception', {
        message: error.message,
        stack: error.stack
      });

      return [transFlag, []];
    }
  }

  async uploadChunk(chunk, uploadID, quickUpload)
  {
    const file = chunk.file
    const [url, params] = this.uploadChunkUrl(chunk.index + 1, uploadID, quickUpload ? 1 : 2);
    const uploadHeader = await this.getUploadHeader(file)

    const response = await axios({
      method: 'PUT',
      url,
      params,
      data: chunk.content,
      timeout: 120 * 1000,
      headers: Object.assign(uploadHeader, {
        'Content-Length': chunk.size,
        'Content-Md5': crypto.createHash('md5').update(chunk.content).digest('hex'),
        'Content-Type': 'application/octet-stream',
      })
    })
    
    console.log('Platform:Weixinshipin:UploadChunk:Response', response.data)

    return response.data;
  }

  async getUploadHeader(file) {
    const fileSize = await file.getFileSize()

    return {
      'Authorization': this.uploadParams.authKey,
      'X-Arguments': [
        `apptype=${this.uploadParams.appType}`,
        `filetype=${this.uploadParams[file.cdnFileType]}`,
        `weixinnum=${this.uploadParams.uin}`,
        `filekey=${encodeURIComponent(file.basename)}`,
        `filesize=${fileSize}`,
        `taskid=${file.uuid}`,
        `scene=${this.uploadParams.scene || 0}`
      ].join('&')
    };
  }

  uploadChunkUrl(index, uploadID, quickUpload = null) {
    const cdnHostList = this.useBackUpHost
    ? (this.envInfo.spareCdnHostList || [])
    : (this.envInfo.cdnHostList || []);

    const cdnHost = cdnHostList.length > 0
      ? cdnHostList[Math.floor(Math.random() * cdnHostList.length)]
      : (this.envInfo.cdnHost || null);

    quickUpload = quickUpload ?? 2;

    return [`https://${cdnHost || this.host}/uploadpartdfs`, {
      PartNumber: index,
      UploadID: uploadID,
      QuickUpload: quickUpload,
    }];
  }

  async getUploadId(file) {
    const fileSize = await file.getFileSize()
    const fileMd5 = await file.getMd5()
    const chunkCount = Math.ceil(fileSize / Uploader.CHUNK_SIZE);

    const blockParts = Array.from({ length: chunkCount }, (_, index) => {
      const n = (index + 1) * Uploader.CHUNK_SIZE;
      return Math.min(n, fileSize);
    });

    const uploadHeader = await this.getUploadHeader(file)

    const { data: response } = await axios({
      method: 'PUT',
      url: `${this.host}/applyuploaddfs`,
      headers: Object.assign(uploadHeader, {
        'content-md5': fileMd5,
      }),
      data: {
        BlockSum: chunkCount,
        BlockPartLength: blockParts,
      },
    })

    console.log('Platform:Weixinshipin:GetUploadId:Response', {
      'BlockSum': chunkCount,
      'BlockPartLength': blockParts,
    }, response)

    return response.UploadID;
  }

  getResponseByCache(filePath, cacheFilePath){
    const records = getCacheRecord(cacheFilePath);
    
    if (records[filePath]) {
      const { content, timestamp } = records[filePath];
      const currentTime = Date.now();

      if (currentTime - timestamp <= Uploader.CACHE_EXPIRATION_TIME) {
        return JSON.parse(content);
      }
    }
  
    return null;
  }

  setHost(host) {
    this.host = host;
    return this;
  }
}